<!DOCTYPE html>
<html lang="zh-cn">

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

    <title>Title</title>
    <style>
        #app {
            width: 400px;
            height: 400px;
            margin: 50px auto 0;
        }

        #input {
            outline: none;
            border: 1px solid #ddd;
            width: 280px;
            display: inline-block;
            line-height: 32px;
            padding: 2px 20px;
            border-radius: 8px;
            margin-top: 30px;
            color: #555;
        }
    </style>
</head>

<body>

<!--<h2>双向绑定示例</h2>-->

<div id="app">
    <div>{{val}}</div>
    <input type="text" id="input" v-model="val"></div>
<script>

    // 发布
    function Sub() {
        this.subs = [];
    }

    Sub.prototype = {
        add(sub) {
            this.subs.push(sub);
        },
        trigger() {
            //debugger
            this.subs.forEach(sub => {
                // 触发 Watcher实例 的方法
                sub.update();
            })
        }
    };
    // Sub.target 的值为 一个 Watcher 实例
    Sub.target = null;

    // 数据劫持
    function observe(data) {
        //debugger
        if (typeof data !== 'object' || !data) return;
        // 遍历$data的key
        Object.keys(data).forEach(item => {
            let val = data[item];
            let sub = new Sub();
            Object.defineProperty(data, item, {
                // 当且仅当该属性的 enumerable 键值为 true 时，该属性才会出现在对象的枚举属性中。
                enumerable: true,
                // 当且仅当该属性的 configurable 键值为 true 时，该属性的描述符才能够被改变，同时该属性也能从对应的对象上被删除
                configurable: false,
                // get方法
                get() {
                    //debugger
                    if (Sub.target) {
                        // 将 Watcher 实例添加到订阅列表
                        sub.add(Sub.target);
                    }
                    return val;
                },
                set(newVal) {
                    //debugger
                    val = newVal;
                    sub.trigger();
                }
            })
        })
    }

    // 观察者
    function Watcher(vm, prop, callback) {
        //debugger
        this.vm = vm;
        this.prop = prop;
        this.callback = callback;
        Sub.target = this;
        // 此处会触发 get 方法
        let val = this.vm.$data[prop];
        Sub.target = null;
        // 这个 value 是 data对象里的 “旧” 值
        this.vaule = val;
    }

    // 将在 data属性 的 set方法 中调用，input 事件触发后重新给 data属性赋值，并且会触发 set 方法
    Watcher.prototype.update = function () {
        //debugger
        let newValue = this.vm.$data[this.prop];
        if (this.value !== newValue) {
            this.value = newValue;
            // callback：val => { node.value = val }
            // 将 新的值 赋给 input元素的 value（call函数的第一个参数可以不是 this.vm 不影响）
            this.callback.call(this.vm, newValue);
        }
    }

    // 编译构造函数
    function Compile(vm) {
        this.vm = vm;
        this.el = vm.$el;
        this.init();
    }

    // 编译初始化
    Compile.prototype.init = function () {
        //debugger
        // 创建了一虚拟的节点对象，节点对象包含所有属性和方法
        let fragment = document.createDocumentFragment();
        // 取到有模版语法的div
        let child = this.el.firstChild;
        while (child) {
            // 依次将 this.el 中的元素 append 到虚拟节点对象，append 一个少一个。当 this.el 没有子元素时，this.el.firstChild = null
            fragment.append(child);
            child = this.el.firstChild;
        }
        // 获取子元素 NodeList
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            //debugger
            // nodeType 等于1 代表元素节点（避开一些空白节点之类的）
            if (node.nodeType === 1) {
                // 获取元素的所有属性节点，一个 ArrayLike
                let attrs = node.attributes;
                Array.from(attrs).forEach(attr => {
                    // 元素属性的名称
                    let name = attr.nodeName;
                    if (name === 'v-model') {
                        // 获取 v-model 上绑定的 key
                        let prop = attr.nodeValue;
                        // 获取 Vue data对象中的值
                        let value = this.vm.$data[prop];
                        // 此处的 node 是 input元素
                        node.value = value;
                        // 生成更改 input元素的 Watcher 实例（传入 Vue实例，data对象 key，更改 input元素 value 的回调函数）
                        new Watcher(this.vm, prop, val => {
                            node.value = val;
                        });
                        // 给 input元素 绑定 input事件，并将输入的值赋给 Vue data对象对应的属性
                        node.addEventListener('input', e => {
                            //debugger
                            let newVal = e.target.value;
                            if (value !== newVal) {
                                // 此处会触发该属性的 set 方法
                                this.vm.$data[prop] = newVal;
                            } else {
                                // 经测试，下面代码应该不会触发，上面判断多余
                                console.log(99999999999999999999)
                            }
                        })
                    }
                })
            }

            let reg = /\{\{(.*)\}\}/;
            // textContent 属性设置或者返回指定节点的文本内容 ==> {{val}}
            let text = node.textContent;
            if (reg.test(text)) {
                // RegExp这个对象会在我们调用了正则表达式的方法后, 自动将最近一次的结果保存在里面, 所以如果我们在使用正则表达式时, 有用到分组, 那么就可以直接在调用完以后直接使用RegExp.$xx来使用捕获到的分组内容
                let prop = RegExp.$1;   // val
                // 将 匹配到的 data属性值 赋给 div
                node.textContent = this.vm.$data[prop];
                // 生成 更改 div 文本的 Watcher 实例
                new Watcher(this.vm, prop, val => {
                    // console.log(node);
                    node.textContent = val;
                });
            }
        })
        this.el.appendChild(fragment);
    }

    // Vue构造函数
    function MyVue(options) {
        this.$options = options;
        this.$el = options.el;
        this.$data = options.data;
        this.init();
    }

    // Vue初始化操作
    MyVue.prototype.init = function () {
        observe(this.$data);
        new Compile(this);
    };


    const vm = new MyVue({
        el: document.getElementById('app'),
        data: {
            val: 123
        }
    })

    window.onload = function () {
        document.getElementById('input').focus();
    }
</script>


</body>

</html>
